module DomainMigrations
  module Table #:nodoc:
    def method_missing(name, *args) 
      # Needed to trap use of domains in create_table:
      # create_table :my_table do |t|
      #   t.my_domain_type :field_name
      # end
      #
      return column(args.first, name) if @base.is_domain(name)
    end
  end
    
  module Schema
    
    def domains
      @domains ||= load_domains
    end

    def is_domain(name)
      domains[name]
    end

    def create_domain(name, type, options = {}, &block)
      dom = ::ActiveRecord::ConnectionAdapters::DomainDefinition.new(self,
        as_symbol(name), type, options[:limit], options[:precision], options[:scale], options[:default], options[:null], &block)
            
      drop_domain(dom.name, options) rescue nil if options[:force]
      options.delete(:force)

      add_domain(dom)      
    end

    def drop_domain(dom_name)
      remove_domain(dom_name)
    end

    private
    def as_symbol(value)
      return value if (value.is_a? Symbol)
      value.to_s.downcase.to_sym
    end
    
    def load_domains      
      fname = File.expand_path(File.join(RAILS_ROOT, 'db/migrate', 'domains.yml'))
      @domains = YAML.load(File.open(fname)) if File.exist?(fname)
      @domains ||= {}
    end
    
    def save_domains
      fname = File.expand_path(File.join(RAILS_ROOT, 'db/migrate', 'domains.yml'))
      File.open(fname, "w") { |f| YAML.dump(@domains, f) }
    end
    
    def add_domain(dom)
      domains[dom.name] = dom
      save_domains
      execute dom.to_sql if ::ActiveRecord::Migration.supports_sql_domains?
    end
    
    def remove_domain(dom_name)
      @domains = domains.delete dom_name
      save_domains
      execute "DROP DOMAIN #{name}" if ::ActiveRecord::Migration.supports_sql_domains?
    end
  end
end

module ::ActiveRecord
 class Migration
    def self.supports_sql_domains(bool)
      @supports_sql_domains = bool
    end
    
    def self.supports_sql_domains?
      @supports_sql_domains ||= false
    end  
  end
  
  module ConnectionAdapters
    class ColumnDefinition
      alias old_to_sql :to_sql
      def to_sql
        return old_to_sql unless base.is_domain(type)
        if Migration.supports_sql_domains?
          return "#{base.quote_column_name(name)} #{sql_type}" 
        else
          dom = base.domains[type]
          dom.name = name
          begin
            sql = dom.old_to_sql
          ensure
            dom.name = type
          end
          sql
        end
      end
      alias to_s :to_sql
    end

    class DomainDefinition < ColumnDefinition
      class Constraint
        def initialize
          @checks = []
        end

        def method_missing(name, *args)
          args = args[0] if args.size == 1
          @checks.push [name, args]
          self
        end

        def &(other)
          right = @checks.pop
          left = @checks.pop
          @checks.push(:and => [left, right])
          self
        end

        def |(other)
          right = @checks.pop
          left = @checks.pop
          @checks.push(:or => [left, right])
          self
        end

        def to_sql
          return '' if (@checks.size == 0)
          fmt(@checks[0])
        end
        alias to_s :to_sql

        private
        def fmt_arg(arg)
          case arg
          when Symbol
            "#{arg.to_s}"
          when String
            "'#{arg}'"
          when Array
            '(' + arg.map{|item| fmt_arg(item)}*', ' + ')'
          else
            arg.to_s
          end
        end

        def fmt(expr)
          case expr
          when Hash
            pair = expr.shift
            "(#{fmt(pair[1][0])}) #{fmt(pair[0])} (#{fmt(pair[1][1])})"
          when Symbol
            "#{expr.to_s}"
          when String
            "'#{expr}'"
          when Array
            op = expr.shift
            need_par = (expr.size > 1)
            s = ' '
            s << '(' if need_par
            s << expr.map{|arg| fmt_arg(arg)}*', '
            s << ')' if need_par
            'VALUE ' + fmt_arg(op) + s
          else
            expr.to_s
          end
        end
      end

      def initialize(base, name, type, limit, precision, scale, default, null, &block)
        super(base, name, type, limit, precision, scale, default, null)
        @constraint = Constraint.new
        check(&block) if block
      end
      
      def to_yaml( opts = {} )
        YAML::quick_emit( object_id, opts ) do |out|
          out.map( taguri, to_yaml_style ) do |map|
            (self.members - ['base']).each do |m|
              map.add(m, self[m]) 
            end
          end
        end
      end
  
      def check(&block)
        self.instance_eval(&block)
        self
      end

      def value
        @constraint
      end

      def to_sql
        column_sql = "CREATE DOMAIN #{base.quote_column_name(name)} AS #{sql_type}"
        add_column_options!(column_sql, :null => null, :default => default)
        check_sql = @constraint.to_sql
        column_sql << " CHECK(#{check_sql})" if check_sql.length > 0
        column_sql
      end
      alias to_s :to_sql
    end
  end

end
